package tss.common;

import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;

public abstract class Connection extends Thread {

	protected Selector selector;					// Selector to that will be monitored

	private Charset charset;						// defining encoding type for creating encoder/decoders
    private CharsetDecoder decoder;
    private CharsetEncoder encoder;
	private ByteBuffer buffer;						// encoded data buffer
	private CharBuffer charBuffer;					// decoded data buffer
	
	private Map<SocketChannel, String> channelPackets;

    public Connection() throws IOException 
    {
		// Setup pending packets queue
		this.channelPackets = new HashMap<SocketChannel, String>();
		
		// Setup buffer/encoder/decoder
    	this.buffer = ByteBuffer.allocateDirect(4096);
    	this.charBuffer = CharBuffer.allocate(4096);
    	this.charset = Charset.forName("ISO-8859-1");
    	this.decoder = charset.newDecoder();
    	this.encoder = charset.newEncoder();

    	// Open Selector
    	this.selector = Selector.open();
    }
    
    public void run() {
    	try {
    	while (selector.select(500) >= 0) 
    	{
    		Set<SelectionKey> readyKeys = selector.selectedKeys();
    		Iterator<SelectionKey> iter = readyKeys.iterator();
    		
    		while (iter.hasNext())
    		{
    			SelectionKey key = iter.next();
    			iter.remove();
    			if (key.isAcceptable()) { channelAccept(key); }
    			if (key.isConnectable()) { channelConnect(key); }
    			else if (key.isReadable()) { channelRead(key); }
    			else if (key.isWritable()) { channelWrite(key); }
    		}
    	}
    	} catch (Exception ex) {
    		System.out.println(ex.getStackTrace());
    		System.exit(-1);
    	}
    }
    
    private void channelAccept(SelectionKey key) throws IOException {
    	if (key.channel() instanceof ServerSocketChannel) {
    		ServerSocketChannel ssc = (ServerSocketChannel)key.channel();    		
    		SocketChannel channel = ssc.accept();
    		channel.configureBlocking(false);
    		channel.register(selector, SelectionKey.OP_READ);
    		
    		System.out.printf("Accepted connection from [%s]\n\n", channel.socket().getRemoteSocketAddress().toString());
    	}
    }
    
    private void channelConnect(SelectionKey key) throws Exception {
    	SocketChannel channel = (SocketChannel)key.channel();
    	channel.finishConnect();

    	onConnect(key);
    }
    
    private void channelRead(SelectionKey key) throws IOException
    {
    	String packet = null;
    	SocketChannel channel = (SocketChannel)key.channel();
    	//Socket socket = channel.socket();
    	//SocketAddress remoteAddr = socket.getRemoteSocketAddress();
    	int numRead = -1;
    	
    	buffer.clear();
    	numRead = channel.read(buffer);
    	
    	if (numRead == -1) {
    		disconnect(key);
    		return;
    	} else {
        	buffer.flip();
        	charBuffer.clear();
        	decoder.decode(buffer, charBuffer, false);
        	packet = charBuffer.flip().toString();
        	onRead(packet, key);
    	}
   }
    
    private void channelWrite(SelectionKey key) throws IOException
    {
		SocketChannel channel = (SocketChannel)key.channel();
		String packet = channelPackets.get(channel);

		channel.write(encoder.encode(CharBuffer.wrap(packet)));
		
		channelPackets.remove(channel);
		key.interestOps(SelectionKey.OP_READ);
    }
    
    protected void disconnect(SelectionKey key) throws IOException {
    	SocketChannel channel = (SocketChannel)key.channel();
    	
		onDisconnect(key);
		
		channel.close();
		key.cancel();
		
		System.out.println("Connection closed by client: " + channel);
    }

    protected void connectServer(int port) throws IOException {
    	// Setup channel (handled by concrete class)
    	ServerSocketChannel channel = ServerSocketChannel.open();
    	channel.configureBlocking(false);
    	InetSocketAddress isa = new InetSocketAddress(port);
    	channel.socket().bind(isa);
    	channel.register(selector, SelectionKey.OP_ACCEPT);
    }
    
    protected void connectClient(String host, int port){
    	try{
			SocketChannel channel = SocketChannel.open();
	    	channel.configureBlocking(false);
	    	channel.connect(new InetSocketAddress(host, port));
	    	channel.register(selector, SelectionKey.OP_CONNECT);
    	}catch(Exception e){
    		e.printStackTrace();
    	}
    }
    
    protected abstract void onAccept(SelectionKey key) throws Exception;
    
    protected abstract void onConnect(SelectionKey key) throws Exception;
    
    protected abstract void onDisconnect(SelectionKey key);
    
    protected abstract void onRead(String packet, SelectionKey key);
    
    protected void write(String packet, SelectionKey key) {
    	// Get channel
    	SocketChannel channel = (SocketChannel)key.channel();
    	// pass through
    	write(packet, channel);
    }
    
    protected void write(String packet, SocketChannel channel) {
    	// Store in queue
    	channelPackets.put(channel, packet);
    	
    	// notify selector of write interest
    	SelectionKey key = channel.keyFor(selector);
    	key.interestOps(SelectionKey.OP_WRITE);
    }
}
